//@author A0081058H
package net.mysoc.w111j.ui.expenselist;

import static net.mysoc.w111j.ui.Constants.DATE_FORMAT;

import java.util.List;

import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.scene.control.Button;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.OverrunStyle;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TextField;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.DataFormat;
import javafx.scene.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.HBoxBuilder;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
import javafx.stage.Popup;
import javafx.util.Callback;
import net.mysoc.w111j.model.Category;
import net.mysoc.w111j.model.Expense;
import net.mysoc.w111j.model.PaymentType;
import net.mysoc.w111j.model.User;
import net.mysoc.w111j.service.ExpenseService;
import net.mysoc.w111j.ui.validation.Validation;
import net.mysoc.w111j.ui.validation.ValidationMessage;
import net.mysoc.w111j.ui.validation.ValidationState;
import net.mysoc.w111j.util.Layout;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The view for the expense list pane.
 */
public class ExpenseListView extends BorderPane {
    private static Logger logger = LoggerFactory.getLogger(ExpenseListView.class);

    private final ExpenseListModel model;
    private ExpenseListPresenter presenter;
    private ExpenseService expenseService;
    private final ListView<Expense> list = new ListView<>();
    private final DataFormat expenseDataFormat = new DataFormat("net.mysoc.w111j.model.Expense");

    /**
     * Constructor. Links an {@ExpenseListModel} to this view.
     *
     * @param model the {@ExpenseListModel} to link this view to.
     */
    public ExpenseListView(ExpenseListModel model) {
        this.model = model;
        buildView();
    }

    /**
     * Sets the {@code ExpenseListPresenter} linked to this view.
     *
     * @param presenter the presenter linked to this view.
     */
    public void setPresenter(ExpenseListPresenter presenter) {
        this.presenter = presenter;
    }

    /**
     * Sets the {@code ExpenseService} used by this view.
     *
     * @param expenseService the {@code ExpenseService} used by this view.
     */
    public void setExpenseService(ExpenseService expenseService) {
        this.expenseService = expenseService;
    }

    /**
     * Builds the view.
     */
    private void buildView() {
        VBox vbox = new VBox();
        ListView<Expense> expenseList = addExpenseList();
        VBox.setVgrow(expenseList, Priority.ALWAYS);
        vbox.getChildren().addAll(addCurrentQuery(), expenseList);
        setCenter(vbox);
        getStyleClass().add("expense-list-view");

    }

    /**
     * Creates the current query {@code Text} for the expense list. Bound to the currentQuery property in the
     * {@code ExpenseListModel}.
     *
     * @return the {@code Text} indicating the current list query.
     */
    private HBox addCurrentQuery() {
        Label currentQuery = new Label();
        currentQuery.textProperty().bind(this.model.listHeaderTextProperty());
        currentQuery.getStyleClass().addAll("expense-list-view-currentquery-text", "typo", "item-subheader");
        currentQuery.setTextOverrun(OverrunStyle.ELLIPSIS);

        return HBoxBuilder.create().children(currentQuery).styleClass("expense-list-view-currentquery-box").build();
    }

    /**
     * Refreshes the category list by rebuilding the sidebar (which contains it).
     */
    public void updateCategoryList() {
        this.model.scrollPaneLocationProperty().unbind();
        this.setLeft(addSidebar());
    }

    /**
     * Creates the expense list sidebar. The sidebar contains the category list.
     *
     * @return the epense list sidebar.
     */
    private HBox addSidebar() {
        VBox filters = new VBox();
        HBox wrapper = new HBox();
        final ScrollPane sidebar = new ScrollPane();
        sidebar.setPrefWidth(235);

        sidebar.setContent(filters);
        filters.getChildren().addAll(addAllExpensesLink(), addCategoryList(), addPaymentTypeList());
        wrapper.getChildren().addAll(sidebar);

        // Do not reset sidebar position on category click
        // FIXME: HACK HACK HACK! Find out why this hackish solution is required, and we can't just simply bind the
        // vValueProperty to the scrollPaneLocationProperty and be done with it.
        sidebar.vvalueProperty().bindBidirectional(this.model.scrollPaneLocationProperty());
        sidebar.vvalueProperty().addListener(new ChangeListener<Object>() {
            @Override
            public void changed(ObservableValue<?> o, Object oldVal, Object newVal) {
                if ((double) newVal == 0.0 && (double) oldVal > 0.00001) {
                    sidebar.setVvalue((double) oldVal);
                } else {
                    sidebar.setVvalue((double) newVal);
                }
            }
        });

        filters.getStyleClass().addAll("expense-list-view-sidebar-filters");
        sidebar.getStyleClass().addAll("expense-list-view-sidebar");
        wrapper.getStyleClass().addAll("expense-list-view-wrapper");

        return wrapper;
    }

    /**
     * Creates the "All" expenses link.
     *
     * @return a {@code VBox} containing the "All" expenses link.
     */
    private VBox addAllExpensesLink() {
        VBox vbox = new VBox();
        HBox allExpensesLinkBox = new HBox();
        Text expensesLabel = new Text("Expenses");

        Rectangle icon = new Rectangle();
        icon.setWidth(30);
        icon.setHeight(30);
        icon.setFill(Layout.stringToHslColor("All"));

        Hyperlink allExpensesLink = new Hyperlink("All");
        allExpensesLink.setPrefWidth(140);
        allExpensesLink.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent e) {
                presenter.handleAllCategories();
            }
        });

        allExpensesLink.getStyleClass().addAll("link", "typo");
        allExpensesLinkBox.getStyleClass().addAll("item");
        expensesLabel.getStyleClass().addAll("expense-list-view-sidebar-expenses-heading", "typo", "item-subheader");
        vbox.getStyleClass().addAll("expense-list-view-sidebar-all");

        allExpensesLinkBox.getChildren().addAll(icon, allExpensesLink);
        vbox.getChildren().addAll(expensesLabel, allExpensesLinkBox);

        return vbox;
    }

    /**
     * Creates the category list.
     *
     * @return the category list.
     */
    private VBox addCategoryList() {
        final VBox categoryListBox = new VBox();
        categoryListBox.setMaxWidth(200);

        Text categoryLabel = new Text("Categories");
        categoryLabel.getStyleClass().addAll("expense-list-view-sidebar-category-heading", "typo", "item-subheader");
        categoryListBox.getChildren().add(categoryLabel);

        // Category list
        List<Category> categories = expenseService.getAllCategories();

        for (final Category category : categories) {
            final HBox categoryLinkBox = new HBox();

            Rectangle categoryAccent = new Rectangle();
            categoryAccent.setWidth(30);
            categoryAccent.setHeight(30);
            categoryAccent.setFill(Layout.stringToHslColor(category.toString()));

            final Hyperlink categoryLink = new Hyperlink(category.toString());
            // FIXME: un-hardcode this (140-30 to account for category controls)
            categoryLink.setPrefWidth(110);
            categoryLink.getStyleClass().addAll("typo", "link");

            categoryLink.setOnAction(new EventHandler<ActionEvent>() {
                @Override
                public void handle(ActionEvent e) {
                    presenter.handleChangeCategory(category);
                }
            });

            final HBox categoryControls = addCategoryControls(category);
            HBox.setHgrow(categoryControls, Priority.ALWAYS);

            categoryLinkBox.setOnMouseEntered(new EventHandler<MouseEvent>() {
                @Override
                public void handle(MouseEvent e) {
                    categoryControls.getStyleClass().remove("controls-inactive");
                    categoryControls.getStyleClass().add("controls-active");
                }
            });

            categoryLinkBox.setOnMouseExited(new EventHandler<MouseEvent>() {
                @Override
                public void handle(MouseEvent e) {
                    categoryControls.getStyleClass().add("controls-inactive");
                    categoryControls.getStyleClass().remove("controls-active");
                }
            });

            // Use wrapper instead of Separator because we want to put drag
            // events on the entire box, not just the link
            final HBox wrapper = new HBox();
            wrapper.getChildren().addAll(categoryAccent, categoryLink);
            HBox.setHgrow(wrapper, Priority.ALWAYS);

            // Drag events are set on wrapper
            wrapper.setOnDragOver(new EventHandler<DragEvent>() {
                @Override
                public void handle(DragEvent event) {
                    if (event.getGestureSource() != wrapper && event.getDragboard().hasContent(expenseDataFormat)) {
                        event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
                    }
                    event.consume();
                }
            });

            wrapper.setOnDragDropped(new EventHandler<DragEvent>() {
                @Override
                public void handle(DragEvent event) {
                    if (event.getGestureSource() != wrapper && event.getDragboard().hasContent(expenseDataFormat)) {
                        event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
                        logger.debug("Dragged to {} {}", wrapper, event.getDragboard().getContent(expenseDataFormat));
                        presenter.handleDragExpenseToCategory(
                                (Expense) event.getDragboard().getContent(expenseDataFormat), category);
                    }
                    event.consume();
                }
            });

            // Visually indicate droppable area
            wrapper.setOnDragEntered(new EventHandler<DragEvent>() {
                @Override
                public void handle(DragEvent event) {
                    if (event.getGestureSource() != wrapper && event.getDragboard().hasContent(expenseDataFormat)) {
                        wrapper.getStyleClass().remove("expense-list-view-category-dragexit");
                        wrapper.getStyleClass().add("expense-list-view-category-dragover");
                    }
                    event.consume();
                }
            });

            wrapper.setOnDragExited(new EventHandler<DragEvent>() {
                @Override
                public void handle(DragEvent event) {
                    if (event.getGestureSource() != wrapper && event.getDragboard().hasContent(expenseDataFormat)) {
                        wrapper.getStyleClass().remove("expense-list-view-category-dragover");
                        wrapper.getStyleClass().add("expense-list-view-category-dragexit");
                    }
                    event.consume();
                }
            });

            categoryLink.getStyleClass().addAll("expense-list-view-category-link", "typo", "link");
            categoryLinkBox.getStyleClass().addAll("item");

            // Cannot delete or edit uncategorised category
            if (category.getId() != User.UNCATEGORISED_ID) {
                categoryLinkBox.getChildren().addAll(wrapper, categoryControls);
                categoryListBox.getChildren().add(categoryLinkBox);
            } else {
                categoryLink.setPrefWidth(170); // No controls, make it wider
                categoryLinkBox.getChildren().addAll(wrapper);
                categoryListBox.getChildren().add(categoryLinkBox);
            }
        }

        // New category field
        final TextField newCategoryField = new TextField();
        newCategoryField.setPromptText("New category");
        newCategoryField.textProperty().bindBidirectional(model.newCategoryFieldProperty());

        final Popup validationPopup = new Popup();
        validationPopup.setAutoHide(true);
        validationPopup.setHideOnEscape(true);

        // Clear new category field on VK_ENTER
        newCategoryField.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                if (presenter.handleCreateNewCategory(model.getNewCategoryField())) {
                    newCategoryField.clear();
                    validationPopup.hide();
                } else {
                    // Failed to update category
                    Label validationLabel = new Label();
                    ValidationMessage message = Validation.validateRequired(model.getNewCategoryField());

                    if (message.getState() == ValidationState.OK) {
                        message = new ValidationMessage(ValidationState.ERROR, "Invalid or duplicate name");
                    }

                    Validation.formatValidationLabel(message, validationLabel);
                    validationPopup.getContent().addAll(validationLabel);

                    // Get absolute position
                    Bounds bounds = newCategoryField.localToScene(newCategoryField.getBoundsInLocal());
                    double x = bounds.getMinX() + newCategoryField.getScene().getWindow().getX();
                    double y = bounds.getMaxY() + newCategoryField.getScene().getWindow().getY() + 20;

                    validationLabel.getStyleClass().addAll("typo", "validation-popup");
                    validationPopup.show(newCategoryField, x, y);
                }
            }
        });

        newCategoryField.focusedProperty().addListener(new ChangeListener<Object>() {
            @Override
            public void changed(ObservableValue<?> o, Object oldVal, Object newVal) {
                if ((boolean) oldVal) {
                    validationPopup.hide();
                }
            }
        });

        newCategoryField.getStyleClass().add("expense-list-view-categorylist-newcategory");
        categoryListBox.getStyleClass().addAll("expense-list-view-categorylist", "itemlist");
        categoryListBox.getChildren().add(newCategoryField);

        return categoryListBox;
    }

    /**
     * Adds the category controls for a given {@code Category}.
     *
     * @param category the {@code Category} to link the controls to.
     * @return a {@code HBox} with controls for a given {@code Category}
     */
    public HBox addCategoryControls(final Category category) {
        final HBox categoryControls = new HBox();
        Button deleteButton = new Button("", new ImageView("trashw12.png"));
        final Button editButton = new Button("", new ImageView("penw12.png"));
        final TextField editCategoryTextField = new TextField();

        final Popup validationPopup = new Popup();
        validationPopup.setAutoHide(true);
        validationPopup.setHideOnEscape(true);

        editButton.setTooltip(new Tooltip("Edit"));
        deleteButton.setTooltip(new Tooltip("Delete"));
        editCategoryTextField.setTooltip(new Tooltip("Enter a new category name."));
        editCategoryTextField.setPromptText("Category name");

        deleteButton.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent e) {
                presenter.handleDeleteCategory(category);
            }
        });

        editButton.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent e) {
                categoryControls.getChildren().add(0, editCategoryTextField);
                categoryControls.getStyleClass().remove("controls-inactive");
                categoryControls.getStyleClass().add("controls-active");
                editCategoryTextField.requestFocus();
            }
        });

        // On edit category field ENTER
        editCategoryTextField.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent e) {
                if (presenter.handleEditCategory(category, editCategoryTextField.getText())) {
                    categoryControls.getStyleClass().add("controls-inactive");
                    categoryControls.getStyleClass().remove("controls-active");
                    categoryControls.getChildren().remove(editCategoryTextField);
                } else {
                    // Failed to update category
                    Label validationLabel = new Label();
                    ValidationMessage message = Validation.validateRequired(editCategoryTextField.getText());

                    if (message.getState() == ValidationState.OK) {
                        message = new ValidationMessage(ValidationState.ERROR, "Invalid or duplicate name");
                    }

                    Validation.formatValidationLabel(message, validationLabel);
                    validationPopup.getContent().addAll(validationLabel);

                    // Get absolute position
                    Bounds bounds = editCategoryTextField.localToScene(editCategoryTextField.getBoundsInLocal());
                    double x = bounds.getMinX() + editCategoryTextField.getScene().getWindow().getX();
                    double y = bounds.getMaxY() + editCategoryTextField.getScene().getWindow().getY() + 20;

                    validationLabel.getStyleClass().addAll("typo", "validation-popup");
                    validationPopup.show(editCategoryTextField, x, y);
                }
            }
        });

        // On losing focus, reset the control status
        editCategoryTextField.focusedProperty().addListener(new ChangeListener<Object>() {
            @Override
            public void changed(ObservableValue<?> o, Object oldVal, Object newVal) {
                // If no longer focused
                if ((boolean) oldVal) {
                    if (Validation.validateRequired(editCategoryTextField.getText()).getState() == ValidationState.OK) {
                        presenter.handleEditCategory(category, editCategoryTextField.getText());
                    }
                    editCategoryTextField.clear();
                    categoryControls.getChildren().add(1, editButton);
                    categoryControls.getStyleClass().add("controls-inactive");
                    categoryControls.getStyleClass().remove("controls-active");
                    categoryControls.getChildren().remove(editCategoryTextField);
                } else {
                    categoryControls.getChildren().remove(editButton);
                }
            }
        });

        // Handle VK_ESC when field has focus
        editCategoryTextField.setOnKeyPressed(new EventHandler<KeyEvent>() {
            @Override
            public void handle(KeyEvent event) {
                KeyCode key = event.getCode();
                if (key == KeyCode.ESCAPE) {
                    editCategoryTextField.getParent().requestFocus();
                }
            }
        });

        categoryControls.getStyleClass().add("controls-inactive");
        categoryControls.getChildren().addAll(editButton, deleteButton);

        return categoryControls;
    }

    //@author A0081189X
    /**
     * Creates the payment type list.
     *
     * @return the payment type list.
     */
    private VBox addPaymentTypeList() {
        final VBox paymentTypeListBox = new VBox();
        paymentTypeListBox.setMaxWidth(200);

        Text paymentTypeLabel = new Text("Payment types");
        paymentTypeLabel.getStyleClass().addAll("expense-list-view-sidebar-category-heading", "typo", "item-subheader");
        paymentTypeListBox.getChildren().add(paymentTypeLabel);

        // Payment Type list
        List<PaymentType> paymentTypes = expenseService.getAllPaymentTypes();

        for (final PaymentType paymentType : paymentTypes) {
            final HBox paymentTypeLinkBox = new HBox();

            Rectangle paymentTypeAccent = new Rectangle();
            paymentTypeAccent.setWidth(30);
            paymentTypeAccent.setHeight(30);
            paymentTypeAccent.setFill(Layout.stringToHslColor(paymentType.toString()));

            final Hyperlink paymentTypeLink = new Hyperlink(paymentType.toString());
            // FIXME: un-hardcode this (140-30 to account for category controls)
            paymentTypeLink.setPrefWidth(110);
            paymentTypeLink.getStyleClass().addAll("typo", "link");

            paymentTypeLink.setOnAction(new EventHandler<ActionEvent>() {
                @Override
                public void handle(ActionEvent e) {
                    presenter.handleChangePaymentType(paymentType);
                }
            });

            final HBox paymentTypeControls = addPaymentTypeControls(paymentType);
            HBox.setHgrow(paymentTypeControls, Priority.ALWAYS);

            paymentTypeLinkBox.setOnMouseEntered(new EventHandler<MouseEvent>() {
                @Override
                public void handle(MouseEvent e) {
                    paymentTypeControls.getStyleClass().remove("controls-inactive");
                    paymentTypeControls.getStyleClass().add("controls-active");
                }
            });

            paymentTypeLinkBox.setOnMouseExited(new EventHandler<MouseEvent>() {
                @Override
                public void handle(MouseEvent e) {
                    paymentTypeControls.getStyleClass().add("controls-inactive");
                    paymentTypeControls.getStyleClass().remove("controls-active");
                }
            });

            // Use wrapper instead of Separator because we want to put drag
            // events on the entire box, not just the link
            final HBox wrapper = new HBox();
            wrapper.getChildren().addAll(paymentTypeAccent, paymentTypeLink);
            HBox.setHgrow(wrapper, Priority.ALWAYS);

            // Drag events are set on wrapper
            wrapper.setOnDragOver(new EventHandler<DragEvent>() {
                @Override
                public void handle(DragEvent event) {
                    if (event.getGestureSource() != wrapper && event.getDragboard().hasContent(expenseDataFormat)) {
                        event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
                    }
                    event.consume();
                }
            });

            wrapper.setOnDragDropped(new EventHandler<DragEvent>() {
                @Override
                public void handle(DragEvent event) {
                    if (event.getGestureSource() != wrapper && event.getDragboard().hasContent(expenseDataFormat)) {
                        event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
                        logger.debug("Dragged to {} {}", wrapper, event.getDragboard().getContent(expenseDataFormat));
                        presenter.handleDragExpenseToPaymentType(
                                (Expense) event.getDragboard().getContent(expenseDataFormat), paymentType);
                    }
                    event.consume();
                }
            });

            // Visually indicate droppable area
            wrapper.setOnDragEntered(new EventHandler<DragEvent>() {
                @Override
                public void handle(DragEvent event) {
                    if (event.getGestureSource() != wrapper && event.getDragboard().hasContent(expenseDataFormat)) {
                        wrapper.getStyleClass().remove("expense-list-view-category-dragexit");
                        wrapper.getStyleClass().add("expense-list-view-category-dragover");
                    }
                    event.consume();
                }
            });

            wrapper.setOnDragExited(new EventHandler<DragEvent>() {
                @Override
                public void handle(DragEvent event) {
                    if (event.getGestureSource() != wrapper && event.getDragboard().hasContent(expenseDataFormat)) {
                        wrapper.getStyleClass().remove("expense-list-view-category-dragover");
                        wrapper.getStyleClass().add("expense-list-view-category-dragexit");
                    }
                    event.consume();
                }
            });

            paymentTypeLink.getStyleClass().addAll("expense-list-view-category-link", "typo", "link");
            paymentTypeLinkBox.getStyleClass().addAll("item");

            // Cannot delete or edit uncategorised category
            if (paymentType.getId() != User.UNSPECIFIED_PAYMENT_TYPE_ID) {
                paymentTypeLinkBox.getChildren().addAll(wrapper, paymentTypeControls);
                paymentTypeListBox.getChildren().add(paymentTypeLinkBox);
            } else {
                paymentTypeLink.setPrefWidth(170); // No controls, make it wider
                paymentTypeLinkBox.getChildren().addAll(wrapper);
                paymentTypeListBox.getChildren().add(paymentTypeLinkBox);
            }
        }

        // New category field
        final TextField newPaymentTypeField = new TextField();
        newPaymentTypeField.setPromptText("New payment type");
        newPaymentTypeField.textProperty().bindBidirectional(this.model.newPaymentTypeFieldProperty());

        final Popup validationPopup = new Popup();
        validationPopup.setAutoHide(true);
        validationPopup.setHideOnEscape(true);

        // Clear new category field on VK_ENTER
        newPaymentTypeField.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                if (presenter.handleCreateNewPaymentType(model.getNewPaymentTypeField())) {
                    newPaymentTypeField.clear();
                    validationPopup.hide();
                } else {
                    // Failed to update category
                    Label validationLabel = new Label();
                    ValidationMessage message = Validation.validateRequired(model.getNewPaymentTypeField());

                    if (message.getState() == ValidationState.OK) {
                        message = new ValidationMessage(ValidationState.ERROR, "Invalid or duplicate name");
                    }

                    Validation.formatValidationLabel(message, validationLabel);
                    validationPopup.getContent().addAll(validationLabel);

                    // Get absolute position
                    Bounds bounds = newPaymentTypeField.localToScene(newPaymentTypeField.getBoundsInLocal());
                    double x = bounds.getMinX() + newPaymentTypeField.getScene().getWindow().getX();
                    double y = bounds.getMaxY() + newPaymentTypeField.getScene().getWindow().getY() + 20;

                    validationLabel.getStyleClass().addAll("typo", "validation-popup");
                    validationPopup.show(newPaymentTypeField, x, y);
                }
            }
        });

        newPaymentTypeField.focusedProperty().addListener(new ChangeListener<Object>() {
            @Override
            public void changed(ObservableValue<?> o, Object oldVal, Object newVal) {
                if ((boolean) oldVal) {
                    validationPopup.hide();
                }
            }
        });

        newPaymentTypeField.getStyleClass().add("expense-list-view-categorylist-newcategory");
        paymentTypeListBox.getStyleClass().addAll("expense-list-view-categorylist", "itemlist");
        paymentTypeListBox.getChildren().add(newPaymentTypeField);

        return paymentTypeListBox;
    }

    /**
     * Adds the payment type controls for a given {@code PaymentType}.
     *
     * @param paymentType the {@code PaymentType} to link the controls to.
     * @return a {@code HBox} with controls for a given {@code PaymentType}
     */
    public HBox addPaymentTypeControls(final PaymentType paymentType) {
        final HBox paymentTypeControls = new HBox();
        Button deleteButton = new Button("", new ImageView("trashw12.png"));
        final Button editButton = new Button("", new ImageView("penw12.png"));
        final TextField editPaymentTypeTextField = new TextField();

        final Popup validationPopup = new Popup();
        validationPopup.setAutoHide(true);
        validationPopup.setHideOnEscape(true);

        editButton.setTooltip(new Tooltip("Edit"));
        deleteButton.setTooltip(new Tooltip("Delete"));
        editPaymentTypeTextField.setTooltip(new Tooltip("Enter a new payment type name."));
        editPaymentTypeTextField.setPromptText("Payment type name");

        deleteButton.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent e) {
                presenter.handleDeletePaymentType(paymentType);
            }
        });

        editButton.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent e) {
                paymentTypeControls.getChildren().add(0, editPaymentTypeTextField);
                paymentTypeControls.getStyleClass().remove("controls-inactive");
                paymentTypeControls.getStyleClass().add("controls-active");
                editPaymentTypeTextField.requestFocus();
            }
        });

        // On edit payment type field ENTER
        editPaymentTypeTextField.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent e) {
                if (presenter.handleEditPaymentType(paymentType, editPaymentTypeTextField.getText())) {
                    paymentTypeControls.getStyleClass().add("controls-inactive");
                    paymentTypeControls.getStyleClass().remove("controls-active");
                    paymentTypeControls.getChildren().remove(editPaymentTypeTextField);
                } else {
                    // Failed to update payment type
                    Label validationLabel = new Label();
                    ValidationMessage message = Validation.validateRequired(editPaymentTypeTextField.getText());

                    if (message.getState() == ValidationState.OK) {
                        message = new ValidationMessage(ValidationState.ERROR, "Invalid or duplicate name");
                    }

                    Validation.formatValidationLabel(message, validationLabel);
                    validationPopup.getContent().addAll(validationLabel);

                    // Get absolute position
                    Bounds bounds = editPaymentTypeTextField.localToScene(editPaymentTypeTextField.getBoundsInLocal());
                    double x = bounds.getMinX() + editPaymentTypeTextField.getScene().getWindow().getX();
                    double y = bounds.getMaxY() + editPaymentTypeTextField.getScene().getWindow().getY() + 20;

                    validationLabel.getStyleClass().addAll("typo", "validation-popup");
                    validationPopup.show(editPaymentTypeTextField, x, y);
                }
            }
        });

        // On losing focus, reset the control status
        editPaymentTypeTextField.focusedProperty().addListener(new ChangeListener<Object>() {
            @Override
            public void changed(ObservableValue<?> o, Object oldVal, Object newVal) {
                // If no longer focused
                if ((boolean) oldVal) {
                    if (Validation.validateRequired(editPaymentTypeTextField.getText()).getState() == ValidationState.OK) {
                        presenter.handleEditPaymentType(paymentType, editPaymentTypeTextField.getText());
                    }
                    editPaymentTypeTextField.clear();
                    paymentTypeControls.getChildren().add(1, editButton);
                    paymentTypeControls.getStyleClass().add("controls-inactive");
                    paymentTypeControls.getStyleClass().remove("controls-active");
                    paymentTypeControls.getChildren().remove(editPaymentTypeTextField);
                } else {
                    paymentTypeControls.getChildren().remove(editButton);
                }
            }
        });

        // Handle VK_ESC when field has focus
        editPaymentTypeTextField.setOnKeyPressed(new EventHandler<KeyEvent>() {
            @Override
            public void handle(KeyEvent event) {
                KeyCode key = event.getCode();
                if (key == KeyCode.ESCAPE) {
                    editPaymentTypeTextField.getParent().requestFocus();
                }
            }
        });

        paymentTypeControls.getStyleClass().add("controls-inactive");
        paymentTypeControls.getChildren().addAll(editButton, deleteButton);

        return paymentTypeControls;
    }

    //@author A0081058H
    /**
     * Creates the expense {@code ListView} containing results of the current search query, along with individual result
     * item views. Bound to the expenseList property in the {@code ExpenseListModel}.
     *
     * Note: Scrollbar width is determined by text size.
     *
     * @return the {@code ListView} containing results of the current search query.
     */
    private ListView<Expense> addExpenseList() {
        list.setCellFactory(new Callback<ListView<Expense>, ListCell<Expense>>() {
            @Override
            public ListCell<Expense> call(ListView<Expense> expenseListView) {
                final ListCell<Expense> cell = new ListCell<Expense>() {
                    /*
                     * (non-Javadoc) This is the view for individual {@code ListCell}s. setgraphic takes in the Node to
                     * be displayed.
                     *
                     * @see javafx.scene.control.Cell#updateItem(java.lang.Object, boolean)
                     */
                    @Override
                    protected void updateItem(final Expense expense, boolean empty) {
                        super.updateItem(expense, empty);

                        if (empty) {
                            setGraphic(null);
                        } else {
                            final HBox item = new HBox();
                            VBox lines = new VBox();
                            HBox topLine = new HBox();
                            HBox middleLine = new HBox();
                            HBox bottomLine = new HBox();
                            final HBox itemControls = addItemControls(expense);

                            Text name = new Text(expense.getName());
                            Text price = new Text("$" + expense.getPrice().toString());
                            Text details = new Text(expense.getDetails());
                            Text date = new Text(DATE_FORMAT.format(expense.getDate()));
                            Text paymentType = new Text(expense.getPaymentType().toString());
                            Text category = new Text(expense.getCategory().toString());
                            Text id = new Text("id." + expense.getId());

                            Rectangle categoryAccent = new Rectangle();
                            categoryAccent.setWidth(10);
                            categoryAccent.setHeight(80);
                            categoryAccent.setFill(Layout.stringToHslColor(expense.getCategory().toString()));

                            item.setOnMouseEntered(new EventHandler<MouseEvent>() {
                                @Override
                                public void handle(MouseEvent e) {
                                    itemControls.getStyleClass().remove("controls-inactive");
                                    itemControls.getStyleClass().add("controls-active");
                                }
                            });

                            item.setOnMouseExited(new EventHandler<MouseEvent>() {
                                @Override
                                public void handle(MouseEvent e) {
                                    itemControls.getStyleClass().add("controls-inactive");
                                    itemControls.getStyleClass().remove("controls-active");
                                }
                            });

                            item.setOnDragDetected(new EventHandler<MouseEvent>() {
                                @Override
                                public void handle(MouseEvent event) {
                                    logger.debug("Dragging {}", expense);
                                    Dragboard db = item.startDragAndDrop(TransferMode.ANY);
                                    ClipboardContent content = new ClipboardContent();
                                    content.put(expenseDataFormat, expense);
                                    db.setContent(content);
                                    event.consume();
                                }
                            });

                            topLine.getChildren().addAll(price, name);
                            middleLine.getChildren().addAll(date, category);
                            bottomLine.getChildren().addAll(id, paymentType, details);
                            lines.getChildren().addAll(topLine, middleLine, bottomLine);
                            HBox.setHgrow(lines, Priority.ALWAYS);
                            item.getChildren().addAll(categoryAccent, lines, itemControls);

                            name.getStyleClass().addAll("expense-list-view-item-name", "typo", "item-header");
                            price.getStyleClass().addAll("expense-list-view-item-price", "typo", "item-subheader");
                            details.getStyleClass().addAll("expense-list-view-item-details", "typo", "tertiary-info");
                            date.getStyleClass().addAll("expense-list-view-item-date", "typo", "item-tertiary-header");
                            paymentType.getStyleClass().addAll("expense-list-view-item-paymenttype", "typo",
                                    "tertiary-info");
                            category.getStyleClass().addAll("expense-list-view-item-paymenttype", "typo",
                                    "item-tertiary-header");
                            id.getStyleClass().addAll("expense-list-view-item-id", "typo", "tertiary-info");

                            itemControls.getStyleClass().addAll("controls-inactive");
                            topLine.getStyleClass().addAll("expense-list-view-item-topline");
                            middleLine.getStyleClass().addAll("expense-list-view-item-middleline");
                            bottomLine.getStyleClass().addAll("expense-list-view-item-bottomline");
                            lines.getStyleClass().addAll("expense-list-view-lines");
                            item.getStyleClass().addAll("expense-list-view-item");

                            getStyleClass().addAll("expense-list-view-item-listcell");

                            setGraphic(item);
                        }
                    }

                    /**
                     * Creates, links to {@code ExpenseListPresenter}, and returns the controls (edit, delete) for the
                     * given {@code Expense}.
                     *
                     * @param expense
                     *        the {@code Expense} the controls are for.
                     * @return the controls for this {@code Expense}.
                     */
                    private HBox addItemControls(final Expense expense) {
                        HBox hbox = new HBox();
                        Button edit = new Button("", new ImageView("penw12.png"));
                        Button delete = new Button("", new ImageView("trashw12.png"));

                        edit.setTooltip(new Tooltip("Edit"));
                        delete.setTooltip(new Tooltip("Delete"));

                        edit.setOnAction(new EventHandler<ActionEvent>() {
                            @Override
                            public void handle(ActionEvent e) {
                                presenter.handleEditExpense(expense);
                            }
                        });

                        delete.setOnAction(new EventHandler<ActionEvent>() {
                            @Override
                            public void handle(ActionEvent e) {
                                presenter.handleDeleteExpense(expense);
                            }
                        });

                        edit.getStyleClass().add("expense-list-view-item-controls-edit");
                        delete.getStyleClass().add("expense-list-view-item-controls-delete");
                        hbox.getStyleClass().add("expense-list-view-item-controls");
                        hbox.getChildren().addAll(edit, delete);

                        return hbox;
                    }
                };

                return cell;
            }
        });

        list.setItems(model.expenseListProperty());
        list.getStyleClass().add("expense-list-view-expenselist");

        return list;
    }

    public ListView<Expense> getListView() {
        return this.list;
    }
}
